iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 16
0

就如同昨天結尾所說的,我們應該要跟 Rails 一樣,用 Task.title 的方式來呼叫方法,而不是用 Taks['title'] 來呼叫,今天我們就來建立真正的 attr_accessor

關於 define_method

在進行實作之前,我們要先聊聊一些 Ruby 的基本觀念,在 Ruby 的世界裡面我們可以動態的來定義一個 method(Defining Methods Dynamically),就像下面程式碼所示

class Amo
  define_method :hola do |my_arg|
    "Hi! #{my_arg}"
  end
end

puts Amo.new.hola('apa')

# 印出Hi! apa

這樣的好處是我們可以在程式執行時,直到最後一刻才決定方法的名稱,這也是像是 Ruby 這 直譯語言 (Interpreted language) 的優勢,所以我們可以利用這樣的技巧來實作

# just_do/sqlite_test.rb

require 'sqlite3'
require 'mavericks/sqlite_model'

class Task < Mavericks::Model::SQLite
  Task.schema.keys.each do |attr|
    define_method attr do
      self[attr.to_s]
    end

    define_method "#{attr}=" do |arg|
      self[attr.to_s] = arg
    end
  end
end

task = Task.new('title': '鐵人30', 'content': '每天一篇文章')
task.save!
Task.all.each { |task| puts task.title }

透過之前建立好的方法 schema,來取得 Table 的欄位,靠著這些欄位列表,利用 define_method 來決定要建立那些 accessor,不過這樣有個缺點,就是會非常的慢,而且你會發現為了能夠使用 accessor 必須在每個 class 都這樣寫,數量一多會非常麻煩,所以要改善一下實作

關於 method_missing

在 Ruby 裡面還有另一個這樣的機制,在物件呼叫一個不存在的 method 時,會去呼叫另一個 method,說起來有點饒舌,我們直接來看一個例子

class Amo
  def method_missing(method, *args)
    puts "You called: #{method}(#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end

# 沒有定義 hola 直接呼叫
Amo.new.hola('jose', 2) {}

出現這樣的結果

You called: hola(jose, 2)
(You also passed it a block)

從這裡我們可以知道,當找不到這個 method 的時候,Ruby 會呼叫 method_missing 來做事,也因為這樣,你可以透過複寫 method_missing 來定義 attr_accessor,有點像之前的 const_missing (還記得他嗎?)

當然每次 mthod_missing 也會有速度的問題,所以為了減少呼叫 的次數,我們還需要在呼叫 mthod_missing 之後,定義那些 attr_accessor

現在就來修改我們的 sqlite_model.rb

# mavericks/lib/mavericks/sqlite_model.rb

module Mavericks
  module Model
    class SQLite

      def method_missing(attr, *args)
        attrs = self.class.schema
        attr = attr.to_s.gsub('=', '')
        if attrs.key?(attr)
          self.class.define_attr(attrs)
          val = args.empty? ?  self.send(attr) : self.send("#{attr}=", args[0])
          return val
        else
          super
        end
      end

      def self.define_attr(attrs)
        attrs.keys.each do |attr|
          add_method_to_get(attr)
          add_method_to_set(attr)
        end
      end

      def self.add_method_to_get(attr)
        define_method attr do
          self[attr.to_s]
        end
      end

      def self.add_method_to_set(attr)
        define_method "#{attr}=" do |arg|
          self[attr.to_s] = arg
        end
      end
      
      # .
      # .
      # (略)

我們在 SQLite 這個 class 裡面加上 method_missing,現在當我們第一次執行 task.title 時,就會跳到 method_missing,因為此時的我們還沒有替 Task 定義任何 attr_accessor,所以進到 method_missing 的第一件事情就是取得 schema

attrs = self.class.schema

接著我們將送進來的 method 做簡單的處理,讓 title= 變成 title

attr = attr.to_s.gsub('=', '')

然後做簡單的檢查,這個 attibute 是不是 schema 的欄位之一,如果不是的話,我們就呼叫 super 執行 method_missing 原本做的事情,如果有包含欄位,就定義這些欄位的 method

最後,我們用 empty? 來檢查 args 有沒有值,如果沒有的話,我們就假設他是想取值(例如: task.ttile),如果有給參數的話,代表想要設定新的值(例如 task.title = '鐵人40')

val = args.empty? ?  self.send(attr) : self.send("#{attr}=", args[0])

最後我們回到 sqlite_test.rb 來測試一下

require 'sqlite3'
require 'mavericks/sqlite_model'

class Task < Mavericks::Model::SQLite
end

# 新增一筆資料 title 為 "鐵人30"
task = Task.new('title': '鐵人30', 'content': '每天一篇文章')
task.save!

# 取得剛剛新增的資料,並且將 title 改為 "鐵人40",然後儲存
task2 = Task.find(1)
task2.title = '鐵人40'
task2.save

# 再取一次,確認 title 是不是改為 "鐵人40"
task3 = Task.find(1)
puts task3.title

ORM 差不多就這樣了?

當然不是,要知道 Rails 做得永遠比我們想像中還要來得多,但你有沒有發現 SQLite 越來越肥大,是不是該做一下整理?並且再改良的更好些,而且 SQLite 畢竟不適合拿來開發網頁用,應該用 PostgreSQL...

嗯,那明天我們就來推出一個威力加強版的 ORM 吧!


上一篇
[DAY 15] 復刻 Rails - 更多的 ORM 實作
下一篇
[DAY 17] 復刻 Rails - ORM-威力加強版
系列文
向 Rails 致敬!30天寫一個網頁框架,再拿來做一個 Todo List30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言